/* *
 * --ライセンスについて--
 *
 * 「本ファイルの内容は Mozilla Public License Version 1.1 (「本ライセンス」)
 * の適用を受けます。
 * 本ライセンスに従わない限り本ファイルを使用することはできません。
 * 本ライセンスのコピーは http://www.mozilla.org/MPL/ から入手できます。
 *
 * 本ライセンスに基づき配布されるソフトウェアは、「現状のまま」で配布されるものであり、
 * 明示的か黙示的かを問わず、いかなる種類の保証も行われません。
 * 本ライセンス上の権利および制限を定める具体的な文言は、本ライセンスを参照してください。
 *
 * オリジナルコードおよび初期開発者は、N_H (h.10x64@gmail.com) です。
 *
 * N_H によって作成された部分の著作権表示は次のとおりです。
 *
 * Copyright (C)N_H 2012
 *
 * このファイルの内容は、上記に代えて、
 * GNU General License version2 以降 (以下 GPL とする)、
 * GNU Lesser General Public License Version 2.1 以降 (以下 LGPL とする)、
 * の条件に従って使用することも可能です。
 * この場合、このファイルの使用には上記の条項ではなく GPL または LGPL の条項が適用されます。
 * このファイルの他者による使用を GPL または LGPL の条件によってのみ許可し、
 * MPL による使用を許可したくない対象者は、上記の条項を削除することでその意思を示し、
 * 上記条項を GPL または LGPL で義務付けられている告知およびその他の条項に置き換えてください。
 * 対象者が上記の条項を削除しない場合、
 * 受領者は MPL または GPL または LGPL ライセンスのいずれによってもこのファイルを
 * 使用することができます。」
 *
 * -- License --
 *
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License。You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND、either express or implied。See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Initial Developer of the Original Code is
 *   N_H (h.10x64@gmail.com).
 *
 * Portions created by the Initial Developer are Copyright (C)N_H 2012
 * the Initial Developer。All Rights Reserved.
 *
 * Alternatively、the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL")、or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above。If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL、and not to allow others to
 * use your version of this file under the terms of the MPL、indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL。If you do not delete
 * the provisions above、a recipient may use your version of this file under
 * the terms of any one of the MPL、the GPL or the LGPL.
 *
 * */
package com.magiciansforest.audio.soundrenderer.logic.mmd;

import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;
import com.jme3.scene.control.Control;
import com.magiciansforest.audio.soundrenderer.event.MovieStatusChangeEvent;
import com.magiciansforest.audio.soundrenderer.event.MovieStatusChangeEventListener;
import com.magiciansforest.audio.soundrenderer.logic.TimeKeeper;

/**
 *
 * @author N_H
 */
public class VMDCameraControl extends AbstractControl implements MovieStatusChangeEventListener {

    private Camera cam;
    private VMD vmd;
    private TimeKeeper timeKeeper;
    private double now;
    private boolean isPaused = true;
    private long last;

    public VMDCameraControl(Camera cam, TimeKeeper timeKeeper, VMD vmd) {
        if (vmd.getCameraMotionCount() == 0 || vmd.getCameraMotionArray().length < 1) {
            throw new IllegalArgumentException("vmd don't have camera key frame");
        }

        this.cam = cam;
        this.timeKeeper = timeKeeper;
        this.vmd = vmd;
    }

    @Override
    public void changeMovieStatus(MovieStatusChangeEvent e) {
        if (e.getStatus() == MovieStatusChangeEvent.STARTED || e.getStatus() == MovieStatusChangeEvent.RENDERING) {
            setPause(false);
            last = System.currentTimeMillis();
        } else {
            setPause(true);
        }
    }

    @Override
    protected void controlRender(RenderManager rm, ViewPort vp) {
    }

    @Override
    protected void controlUpdate(float tpf) {
        if (tpf > 0 && !isPaused) {
            setFramePosition(timeKeeper.getMovieFramePosition());
        }
    }

    @Override
    public Control cloneForSpatial(Spatial spatial) {
        throw new UnsupportedOperationException("Not supported yet.");
    }
    
    public String getVMDFilePath() {
        if (vmd == null) {
            return null;
        }
        
        return vmd.getFilePath();
    }
    
    public String getVMDFileName() {
        if (vmd == null) {
            return null;
        }
        
        return vmd.getFileName();
    }

    public void setFramePosition(double movieFramePosition) {
        this.now = movieFramePosition / TimeKeeper.MMD_FPS;
        
        if (isEnabled()) {
            int lessIndex = interpolationSearch(vmd.getCameraMotionArray(), movieFramePosition);
            if (lessIndex < 0) {
                setCameraMotion(vmd.getCameraMotionArray()[0]);
            } else if (lessIndex >= vmd.getCameraMotionArray().length - 1) {
                setCameraMotion(vmd.getCameraMotionArray()[vmd.getCameraMotionArray().length - 1]);
            } else {
                setCameraMotion(vmd.getCameraMotionArray()[lessIndex], vmd.getCameraMotionArray()[lessIndex + 1], movieFramePosition);
            }
        }
    }

    public void setPause(boolean isPaused) {
        this.isPaused = isPaused;
    }

    private void setCameraMotion(VMDCameraMotion motion) {
        setCameraMotion(
                motion.getPosX(),
                motion.getPosY(),
                motion.getPosZ(),
                motion.getDistance(),
                motion.getRotX(),
                motion.getRotY(),
                motion.getRotZ(),
                motion.getViewAngle());
    }

    private void setCameraMotion(VMDCameraMotion prev, VMDCameraMotion next, double now) {
        double pos = (now - prev.getFrameNo()) / (next.getFrameNo() - prev.getFrameNo());
        //Ratio X
        double rX = bezierInterpolation(
                0,
                prev.getBezier()[VMDCameraMotion.X_Y0],
                prev.getBezier()[VMDCameraMotion.X_Y1],
                127,
                pos) / 127;
        //Ratio Y
        double rY = bezierInterpolation(
                0,
                prev.getBezier()[VMDCameraMotion.Y_Y0],
                prev.getBezier()[VMDCameraMotion.Y_Y1],
                127,
                pos) / 127;
        //Ratio Z
        double rZ = bezierInterpolation(
                0,
                prev.getBezier()[VMDCameraMotion.Z_Y0],
                prev.getBezier()[VMDCameraMotion.Z_Y1],
                127,
                pos) / 127;
        //Ratio Rotation angle
        double rR = bezierInterpolation(
                0,
                prev.getBezier()[VMDCameraMotion.ROT_Y0],
                prev.getBezier()[VMDCameraMotion.ROT_Y1],
                127,
                pos) / 127;
        //Ratio Distance
        double rD = bezierInterpolation(
                0,
                prev.getBezier()[VMDCameraMotion.DIST_Y0],
                prev.getBezier()[VMDCameraMotion.DIST_Y1],
                127,
                pos) / 127;
        //Ratio ViewAngle
        double rV = bezierInterpolation(
                0,
                prev.getBezier()[VMDCameraMotion.ANGL_Y0],
                prev.getBezier()[VMDCameraMotion.ANGL_Y1],
                127,
                pos) / 127;

        float posX = (float) (rX * next.getPosX() + (1.0 - rX) * prev.getPosX());
        float posY = (float) (rY * next.getPosY() + (1.0 - rY) * prev.getPosY());
        float posZ = (float) (rZ * next.getPosZ() + (1.0 - rZ) * prev.getPosZ());
        float rotX = (float) (rR * next.getRotX() + (1.0 - rR) * prev.getRotX());
        float rotY = (float) (rR * next.getRotY() + (1.0 - rR) * prev.getRotY());
        float rotZ = (float) (rR * next.getRotZ() + (1.0 - rR) * prev.getRotZ());
        float dist = (float) (rD * next.getDistance() + (1.0 - rD) * prev.getDistance());
        float angle = (float) (rV * next.getViewAngle() + (1.0 - rV) * prev.getViewAngle());

        setCameraMotion(posX, posY, posZ, dist, rotX, rotY, rotZ, angle);
    }

    private void setCameraMotion(float posX, float posY, float posZ, float dist, float rotX, float rotY, float rotZ, float viewAngle) {
        Vector3f camPosition = new Vector3f(posX, posY, posZ);
        Vector3f camPosture = new Vector3f(0, 0, -dist);

        Quaternion camDirection = new Quaternion(new float[]{rotX, rotY, -rotZ});
        Vector3f camLocation = camDirection.mult(camPosture).add(camPosition);

        cam.setLocation(camLocation);
        cam.lookAt(camPosition, camDirection.mult(new Vector3f(0, 1, 0)));
        cam.setFrustumPerspective(viewAngle, (float) cam.getWidth() / cam.getHeight(), cam.getFrustumNear(), cam.getFrustumFar());
    }

    private double bezierInterpolation(double y0, double y1, double y2, double y3, double pos) {
        return y0 * Math.pow(1.0 - pos, 3)
                + 3 * y1 * pos * Math.pow(1.0 - pos, 2)
                + 3 * y2 * Math.pow(pos, 2) * (1.0 - pos)
                + y3 * Math.pow(pos, 3);
    }

    /**
     * 
     * @return index of the data that has equals or less than frameNo 
     */
    private int interpolationSearch(VMDCameraMotion[] data, double frameNo) {
        if (data[data.length - 1].getFrameNo() <= frameNo) {
            return data.length - 1;
        } else if (data[0].getFrameNo() >= frameNo) {
            if (data[0].getFrameNo() == frameNo) {
                return 0;
            } else {
                return -1;
            }
        }

        int ret, lessIndex = 0, moreIndex = data.length - 1;
        while (true) {
            ret = (int) Math.round((moreIndex - lessIndex) * (frameNo - data[lessIndex].getFrameNo()) / (data[moreIndex].getFrameNo() - data[lessIndex].getFrameNo())) + lessIndex;
            if (data[ret].getFrameNo() < frameNo) {
                lessIndex = ret + 1;
                if (data[lessIndex].getFrameNo() > frameNo) {
                    return ret; // == "return lessIndex - 1;"
                }
            } else if (data[ret].getFrameNo() > frameNo) {
                moreIndex = ret - 1;
                if (data[moreIndex].getFrameNo() <= frameNo) {
                    return moreIndex;
                }
            } else {
                return ret;
            }
        }
    }
}
